生命周期 ⌚️
> Last Format Time:7/30/2026 16:21:17
> 适用范围:React 16.3+ 的新生命周期模型;以 React 19.2 官方文档为准。
先说结论
- “生命周期方法”主要是类组件的概念。函数组件没有实例,也没有与每个类生命周期一一对应的方法。
- 函数组件的函数体属于渲染阶段;
useEffect、useLayoutEffect等 Effect 属于提交后的同步过程。Effect 应按“与外部系统同步”的思路理解,而不是机械地模拟componentDidMount等方法。 - React 的一次工作可粗分为:触发更新 → 渲染(Render)→ 提交(Commit)→ 浏览器绘制(Paint)。
- 渲染阶段必须保持纯粹。并发渲染下,React 可能暂停、重启或放弃一次渲染,所以“函数体执行了”不等于“这次结果已提交到 DOM”。
- 类组件仍受支持,但 React 官方推荐新代码使用函数组件。
一、完整调用顺序
挂载 Mounting
constructor
↓
static getDerivedStateFromProps
↓
render
↓
React 更新 DOM 和 ref
↓
componentDidMount
前三个处于渲染阶段,componentDidMount 处于提交阶段。
更新 Updating
更新可能由 props、state 或 context 变化触发:
static getDerivedStateFromProps
↓
shouldComponentUpdate
├─ false → 跳过本组件本次 render、getSnapshotBeforeUpdate、componentDidUpdate
└─ true
↓
render
↓
getSnapshotBeforeUpdate
↓
React 更新 DOM 和 ref
↓
componentDidUpdate
注意:shouldComponentUpdate 是性能优化提示,不适合用来保证业务逻辑正确性。调用 forceUpdate() 会跳过它。
卸载 Unmounting
componentWillUnmount
↓
组件从页面移除
后代渲染出错 Error Handling
后代在渲染期间抛错
↓
static getDerivedStateFromError
↓
render 降级 UI
↓
提交降级 UI
↓
componentDidCatch
二、父子组件之间的执行顺序
以下顺序以 Parent → Child → Grandchild 三层结构、未开启 Strict Mode、一次同步更新成功提交为前提。兄弟组件按 JSX 中的先后顺序逐棵深度优先处理。
必须先区分两个阶段
- 渲染阶段:父 → 子,深度优先。 父组件先计算出子组件元素,React 才知道接下来需要渲染哪些子组件。
- 提交阶段:不能简单概括为永远“子 → 父”。 挂载完成回调通常是子 → 父;整棵子树真正删除时,卸载清理是父 → 子;更新时还会根据 Effect 类型和节点是否被删除产生不同顺序。
渲染阶段可能被暂停、重试或放弃,提交阶段才表示这次结果真正生效。因此不能通过父子生命周期的先后顺序传递业务数据。
类组件首次挂载
Parent constructor
Parent getDerivedStateFromProps
Parent render
Child constructor
Child getDerivedStateFromProps
Child render
Grandchild constructor
Grandchild getDerivedStateFromProps
Grandchild render
Grandchild componentDidMount
Child componentDidMount
Parent componentDidMount
可记为:
- 渲染阶段的
constructor → getDerivedStateFromProps → render:父 → 子; - 提交阶段的
componentDidMount:子 → 父。
若有两个兄弟 ChildA、ChildB,常见顺序是先完成 A 的整棵渲染,再完成 B 的整棵渲染;挂载提交顺序为 ChildA componentDidMount → ChildB componentDidMount → Parent componentDidMount。
类组件父子同时更新
假设父组件更新并向子组件传入新 props:
Parent getDerivedStateFromProps
Parent shouldComponentUpdate
Parent render
Child getDerivedStateFromProps
Child shouldComponentUpdate
Child render
Grandchild ...
Grandchild getSnapshotBeforeUpdate
Child getSnapshotBeforeUpdate
Parent getSnapshotBeforeUpdate
React 修改 DOM
Grandchild componentDidUpdate
Child componentDidUpdate
Parent componentDidUpdate
即:
- 更新渲染阶段:父 → 子;
getSnapshotBeforeUpdate:子 → 父;componentDidUpdate:子 → 父。
如果 Parent.shouldComponentUpdate() 返回 false,由这次父级更新引起的父组件后续渲染和子树遍历都会跳过;但子组件仍可能因为自己的 state、context 或其他更新源而独立更新。
类组件卸载
父组件连同整棵子树一起卸载
Parent componentWillUnmount
Child componentWillUnmount
Grandchild componentWillUnmount
是父 → 子,不要和挂载时的 componentDidMount 顺序混淆。
父组件更新时只移除 Child
Parent render
Parent getSnapshotBeforeUpdate // 如果实现了
Child componentWillUnmount
Parent componentDidUpdate
Child 的后代也会在 Child 之后继续按父 → 子执行 componentWillUnmount。
函数组件首次挂载
Parent 函数体
Child 函数体
Grandchild 函数体
Grandchild useInsertionEffect setup
Child useInsertionEffect setup
Parent useInsertionEffect setup
Grandchild useLayoutEffect setup
Child useLayoutEffect setup
Parent useLayoutEffect setup
浏览器通常完成绘制
Grandchild useEffect setup
Child useEffect setup
Parent useEffect setup
可记为:
- 函数组件执行(渲染阶段):父 → 子;
useInsertionEffect、useLayoutEffect、useEffect的首次 setup:子 → 父;useLayoutEffect在绘制前同步执行;useEffect通常在绘制后执行。
函数组件更新
父子共同重新渲染时,函数体仍是父 → 子。对于每一个依赖发生变化的 Effect,React 保证:
该 Effect 上一轮 cleanup(旧 props/state)
↓
该 Effect 新一轮 setup(新 props/state)
React DOM 19.2.x 中,多层组件常见的 useEffect 更新顺序是:
Child useEffect cleanup
Parent useEffect cleanup
Child useEffect setup
Parent useEffect setup
但只应把“同一个 Effect 的 cleanup 一定先于它的下一次 setup”当作业务语义。不要让父 Effect 必须等待子 Effect,或让子 Effect 必须依赖父 Effect 已执行;React 官方没有把跨组件 Effect 的完整遍历顺序定义为组件通信契约。
useInsertionEffect 和 useLayoutEffect 在更新提交中的清理、建立时机与 useEffect 不完全相同,多个组件之间还可能交错。业务代码应依赖各自的 setup/cleanup 对称性,而不是背诵一条统一顺序。
函数组件卸载
整棵子树真正从 React 树中删除时,React DOM 19.2.x 的常见顺序是父 → 子:
Parent useLayoutEffect cleanup
Child useLayoutEffect cleanup
Grandchild useLayoutEffect cleanup
Parent useEffect cleanup
Child useEffect cleanup
Grandchild useEffect cleanup
如果父组件更新时只删除 Child,则 Child 子树的布局 Effect 在提交删除时清理,被动 useEffect 的 cleanup 随后在被动 Effect 阶段执行。
> 面试中经常出现“Effect cleanup 永远子 → 父”的错误结论。依赖变化时的重新同步与节点真正卸载是两条不同路径;最稳妥的回答是先说明场景,再给出当前 React DOM 的可观察顺序,并强调跨组件顺序不应用作业务契约。
Strict Mode 对日志顺序的影响
开发环境启用 <StrictMode> 后,组件函数、部分纯生命周期以及 Effect setup/cleanup 会额外执行检查。因此控制台可能看到重复的父子渲染,以及挂载后的 setup → cleanup → setup。生产环境没有这轮额外检查;分析基础顺序时应先关闭 Strict Mode,排除检查日志的干扰。
父子组件顺序速记
| 场景 | 方向 |
|---|---|
| 类组件/函数组件的渲染阶段 | 父 → 子,深度优先 |
componentDidMount | 子 → 父 |
getSnapshotBeforeUpdate | 子 → 父 |
componentDidUpdate | 子 → 父 |
整棵类组件子树的 componentWillUnmount | 父 → 子 |
| 函数组件首次 Effect setup | 子 → 父 |
| 同一个 Effect 依赖变化 | 旧 cleanup → 新 setup |
| 整棵函数组件子树真正卸载时的 Effect cleanup | React DOM 19.2.x 中通常父 → 子;不要作为跨组件通信契约 |
三、类组件全部生命周期 API
> render 是类组件唯一必需的方法,其余均可选。constructor 严格说是 JavaScript 构造器,但面试中通常与生命周期一起讨论。
constructor(props):实例初始化
时机: 挂载前调用。
用途
- 初始化
state; - 绑定实例方法的
this。使用类字段和箭头函数后通常不再需要它。
约束
- 其他语句之前先调用
super(props); - 除类字段初始化外,只在这里直接给
this.state赋值;不能在这里调用setState; - 必须纯粹,不要请求数据、订阅或操作 DOM;
- 服务端渲染会执行
constructor和render,但不会执行挂载、更新、卸载相关的提交阶段生命周期。
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
}
static getDerivedStateFromProps(props, state):由 props 派生 state
时机: 初次挂载和每次更新时,在 render 之前调用。
返回值: 返回 state 更新对象,或返回 null 表示不更新。
约束
- 必须是静态、纯函数,不能访问组件实例
this,不能执行副作用; - 每次父组件重新渲染时都可能调用,不只在 props 变化时调用;
- 这是少见场景。滥用会产生“双数据源”和 state 与 props 不同步问题。
更常见的替代方案:
- 仅计算派生值:直接在渲染期间计算,昂贵计算再考虑
useMemo; - props 变化后执行副作用:
componentDidUpdate/useEffect; - props 变化时重置全部内部状态:通过变化的
key让 React 重新挂载组件; - 需要保留上一轮 props:优先重新设计数据结构,确有必要再在渲染期间调整 state。
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return {
prevUserId: props.userId,
selectedItem: null,
};
}
return null;
}
render():计算 UI
时机: 挂载和更新的渲染阶段。
作用: 根据 props、state、context 返回 React 节点。
约束
- 必须是纯函数:相同输入应得到相同输出;
- 不能订阅、请求、调用
setState或读写 DOM; - React 可以多次调用或放弃某次渲染,只有提交完成才代表 UI 生效;
shouldComponentUpdate返回false时,本次更新会跳过render。
可返回 React 元素、字符串、数字、Portal、节点数组,以及空节点 null、undefined、true、false。
componentDidMount():挂载提交完成
时机: 组件加入屏幕后调用一次(每次实际挂载一次;不是整个应用全局只执行一次)。
用途: 建立订阅、连接外部系统、操作 DOM。数据请求也能放在这里,但现代框架的数据加载或客户端缓存方案通常更合适。
约束
- 在这里读取了会变化的 props/state,通常还要在
componentDidUpdate处理变化; - 建立的订阅、定时器、连接必须在
componentWillUnmount对称清理; - 可调用
setState,但会额外渲染一次,应只用于测量 DOM 等少数场景。
shouldComponentUpdate(nextProps, nextState, nextContext):跳过不必要更新
时机: 收到新 props/state 后、渲染前;初次挂载和 forceUpdate() 时不调用。
返回值: 默认语义相当于返回 true;返回 false 会跳过本组件本次后续渲染流程。
约束
- 必须纯粹,不能产生副作用或调用
setState; - 不建议手写深比较或
JSON.stringify,可能比渲染更慢; - 类组件通常优先继承
PureComponent;函数组件对应memo; - 不能把返回
false当成阻止渲染的业务保证,React 官方只把它定位为性能优化。
PureComponent 会对 props 和 state 的每个字段做浅比较。因此不可直接修改原对象后继续复用同一引用。
getSnapshotBeforeUpdate(prevProps, prevState):提交 DOM 前取快照
时机: render 之后、React 修改 DOM 之前。
返回值: 任意快照值或 null;该值会成为 componentDidUpdate 的第三个参数。
用途: 在 DOM 改变前读取滚动位置等信息,再在 DOM 改变后恢复。
约束
shouldComponentUpdate返回false时不会调用;- 函数组件目前没有精确等价 Hook;
- 不要在
render或UNSAFE_componentWillUpdate中读取这类 DOM 快照,因为并发渲染下渲染与提交之间可能存在时间间隔。
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.items.length < this.props.items.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
componentDidUpdate(prevProps, prevState, snapshot):更新提交完成
时机: 更新后的 DOM 已提交时调用;初次挂载不调用。
参数
prevProps:更新前 props;prevState:更新前 state;snapshot:getSnapshotBeforeUpdate的返回值,未实现该方法时为undefined。
约束
shouldComponentUpdate返回false时不会调用;- 调用
setState前必须做前后值判断,否则容易无限更新; - 常与
componentDidMount、componentWillUnmount组成一套对称的同步逻辑。
componentDidUpdate(prevProps) {
if (prevProps.roomId !== this.props.roomId) {
this.disconnect(prevProps.roomId);
this.connect(this.props.roomId);
}
}
componentWillUnmount():卸载前清理
时机: 组件从屏幕移除前调用。
用途: 取消订阅、清除定时器、断开连接、取消仍可取消的请求等。
约束
- 清理逻辑应与
componentDidMount的建立逻辑对称; - 不应调用
setState,组件不会再次渲染; - 不能假设它会在浏览器崩溃、进程结束等非正常退出场景执行。
static getDerivedStateFromError(error):错误降级 UI
时机: 后代组件在渲染期间抛错后调用。
返回值: state 更新对象,用于下一次渲染显示降级 UI。
约束: 必须纯粹,不在这里上报错误;日志上报放到 componentDidCatch。
componentDidCatch(error, info):记录后代错误
时机: 后代渲染错误被捕获、降级 UI 提交后调用。
参数: error 是抛出的值;info.componentStack 是组件栈。
定义 getDerivedStateFromError 和/或 componentDidCatch 的组件称为 Error Boundary(错误边界)。
错误边界能捕获后代在以下位置抛出的错误:
- 渲染期间;
- 构造器;
- 生命周期方法。
它通常不能捕获:
- 自身内部抛出的错误;
- 事件处理函数中的错误(应用普通
try...catch); setTimeout等异步回调中的错误;- 服务端渲染中的错误。
函数组件目前没有内置的、与 componentDidCatch 完全等价的 Hook,通常保留一个类错误边界或使用封装库。
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
reportError(error, info.componentStack);
}
render() {
return this.state.hasError
? <p>页面出现异常</p>
: this.props.children;
}
}
四、不安全/已废弃生命周期
以下旧名称已经废弃:
componentWillMount→UNSAFE_componentWillMountcomponentWillReceiveProps→UNSAFE_componentWillReceivePropscomponentWillUpdate→UNSAFE_componentWillUpdate
它们在异步/并发渲染中不安全:渲染工作可能被重复、暂停或放弃,因此不能依赖它们“只执行一次”,也不能在其中执行副作用。新代码不要使用。
UNSAFE_componentWillMount()
- 挂载前调用,历史上位于
constructor与render之间; - 初始化 state 放到字段或
constructor;副作用放到componentDidMount; - 服务端渲染时它是除
constructor、render外还会执行的旧生命周期,因此更容易导致服务端/客户端行为不一致。
UNSAFE_componentWillReceiveProps(nextProps, nextContext)
- 已挂载组件接收父级新 props 时调用;初次挂载不调用;
- 父组件重新渲染即可触发,不能以“值一定改变”为前提;
- 根据 props 计算数据应在渲染期间完成;需要重置 state 时优先使用受控组件或
key。
UNSAFE_componentWillUpdate(nextProps, nextState)
- 更新渲染前调用,初次挂载不调用;
- 不能调用
setState;不能可靠地在此读取更新前 DOM; - DOM 快照应使用
getSnapshotBeforeUpdate,提交后的副作用使用componentDidUpdate。
如果类中实现了 static getDerivedStateFromProps 或 getSnapshotBeforeUpdate,React 不会调用这些 UNSAFE_ 旧生命周期。
五、函数组件如何正确理解“生命周期”
函数组件函数体:对应渲染阶段,不等于 render() 的一次提交
function Profile({ user }) {
const [tab, setTab] = useState('posts');
const title = `${user.name} - ${tab}`;
return <h1>{title}</h1>;
}
函数体应保持纯粹。不要在其中请求、订阅、写 DOM 或注册定时器。
初始化状态:useState / useReducer
函数组件没有 constructor 的精确对应物。昂贵的初始计算使用惰性初始化函数:
const [state, setState] = useState(() => createInitialState(props));
初始化函数也必须纯粹;Strict Mode 开发环境可能调用两次来检查纯度。
useEffect:与外部系统同步
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
对于很多场景,一个 Effect 的 setup/cleanup 可以覆盖类组件中分散在以下三处的逻辑:
componentDidMount:建立连接;componentDidUpdate:依赖改变时先清理旧连接,再建立新连接;componentWillUnmount:清理最后的连接。
但它们并不完全等价:
useEffect在初次提交后也会执行,不能直接表示“只在更新后、不在挂载后”;- 依赖变化时,React 会先以旧值执行 cleanup,再以新值执行 setup;
- 卸载时执行最后一次 cleanup;
useEffect通常允许浏览器先绘制,视觉测量/同步布局应考虑useLayoutEffect;- Effect 只在客户端执行,服务端渲染不执行。
useLayoutEffect:绘制前同步布局
执行在 DOM 提交后、浏览器重新绘制前,适合测量 DOM 并立刻修正布局。它会阻塞绘制,应优先使用 useEffect,仅在避免闪烁或同步测量时使用。
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setHeight(rect.height);
}, []);
它比 useEffect 更接近类组件 componentDidMount / componentDidUpdate 的提交时机,但仍不是 getSnapshotBeforeUpdate 的替代品。
useInsertionEffect:CSS-in-JS 库专用
它会在其他布局 Effect 运行前插入动态样式。普通业务组件不应使用;其主要受众是运行时 CSS-in-JS 库作者。此时 ref 还未附加,也不能在其中更新 state。
性能优化:memo、useMemo、useCallback
| 类组件 | 函数组件 | 准确含义 |
|---|---|---|
PureComponent / shouldComponentUpdate | memo | 在 props 未变化时尝试跳过组件重新渲染 |
| 在实例字段中缓存结果 | useMemo | 缓存一次计算结果 |
| 稳定的实例方法 | useCallback | 缓存函数引用 |
它们都是性能优化工具,不是保证语义正确的生命周期机制。useMemo、useCallback 也不能用来承载副作用。
没有精确 Hook 对应物的生命周期
| 类 API | 函数组件结论 |
|---|---|
getSnapshotBeforeUpdate | 目前没有精确等价 Hook,必要时保留类组件 |
componentDidCatch / 错误边界 | 目前没有内置函数组件错误边界 Hook,使用类边界或封装库 |
getDerivedStateFromProps | 通常在渲染期间直接计算、使用 key 重置状态或重新设计状态结构 |
仅更新时运行的 componentDidUpdate | useEffect 默认挂载后也运行;可用 ref 跳过首轮,但先确认需求是否合理 |
六、Effect 依赖项完整规则
| 写法 | setup 执行时机 | cleanup 执行时机 |
|---|---|---|
| 省略第二个参数 | 每次提交后 | 下一次 setup 前;卸载时 |
空数组 [] | 该次挂载提交后 | 卸载时 |
[dep1, dep2] | 挂载提交后;任一依赖与上次不同时 | 依赖变化后的新 setup 前;卸载时 |
更严谨地说,[] 表示“这个 Effect 没有响应式依赖”,不是“整个应用生命周期中绝对只执行一次”。组件因 key 变化、条件渲染或路由切换而重新挂载时会再次执行;Strict Mode 开发环境还会额外执行一次 setup → cleanup → setup 检查。
依赖是如何比较的
- React 使用
Object.is逐项比较新旧依赖,不是递归深比较; - 依赖列表必须写成固定长度的内联数组,如
[a, b]; - props、state,以及组件函数体内声明并被 Effect 读取的变量和函数,都属于响应式值,应列入依赖;
- 不要为了控制执行次数故意漏依赖,应启用
eslint-plugin-react-hooks的exhaustive-deps规则。
对象和函数依赖
每次渲染新建的对象、数组、函数具有新引用,Object.is 比较会判定其变化。优先按以下顺序处理:
- 删除本来就不需要的 Effect;
- 把对象或函数移到 Effect 内部创建;
- 依赖真正使用的基础类型字段;
- 确有共享稳定引用的需要时,再用
useMemo/useCallback。
不要为了“让依赖稳定”而无条件缓存一切。
闭包与旧值
每次渲染都有自己的 props、state 和闭包。Effect 捕获的是创建它的那次渲染中的值:
// 错误:count 被固定为首次渲染的值
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// 正确:函数式更新不读取外部 count
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
需要读取最新值但不希望它触发 Effect 重新同步时,应先判断这段逻辑是否属于非响应式事件;可使用 ref,或在支持的 React 版本中使用 useEffectEvent。不能把这当作随意绕过依赖规则的手段。
七、Strict Mode 下为什么像“执行了两次”
<StrictMode> 只在开发环境增加检查,不影响生产构建。常见行为包括:
- 额外调用组件函数、类
constructor、render、shouldComponentUpdate等应保持纯粹的逻辑,以发现副作用; - 初次挂载时额外执行一次 Effect 的 setup → cleanup,再执行真实 setup;
- 类组件会出现
componentDidMount→componentWillUnmount→componentDidMount的检查序列; - ref callback 也会额外执行 setup/cleanup 检查。
正确修复方式是让渲染保持纯粹、让 cleanup 完整撤销 setup,而不是用全局标记阻止第二次执行。
八、面试速记表
| 阶段 | 类组件 API | 能否产生副作用 | 函数组件常见方案 |
|---|---|---|---|
| 挂载前 | constructor | 否 | useState / useReducer 惰性初始化 |
| 挂载/更新渲染前 | getDerivedStateFromProps | 否 | 渲染期间派生;必要时 key 重置 |
| 渲染 | render | 否 | 函数组件函数体 |
| 更新判断 | shouldComponentUpdate | 否 | memo |
| DOM 更新前快照 | getSnapshotBeforeUpdate | 只读 DOM | 无精确 Hook 对应物 |
| 挂载提交后 | componentDidMount | 可以 | useEffect / useLayoutEffect |
| 更新提交后 | componentDidUpdate | 可以 | 依赖明确的 useEffect / useLayoutEffect |
| 卸载前 | componentWillUnmount | 只做清理 | Effect cleanup |
| 错误降级 | getDerivedStateFromError | 否 | 无内置函数边界 Hook |
| 错误记录 | componentDidCatch | 可以 | 类错误边界或封装库 |
| 遗留、不安全 | 三个 UNSAFE_ 方法 | 不要使用 | 迁移到现行 API |
九、常见追问
useEffect(fn, []) 是否等于 componentDidMount?
不严格相等。它表示无响应式依赖的 Effect:客户端挂载提交后执行 setup,卸载时 cleanup;重新挂载会再次执行,Strict Mode 开发环境还会多一次检查循环。
cleanup 是否只在卸载时执行?
不是。依赖变化时,新 setup 执行前会先运行上一轮 cleanup;卸载时再运行最后一轮 cleanup。
useLayoutEffect 与 useEffect 的核心区别?
useLayoutEffect 在浏览器绘制前同步执行并阻塞绘制;useEffect 通常允许浏览器先绘制。默认用 useEffect,需要测量并同步修正布局以避免闪烁时才用 useLayoutEffect。
React 生命周期的本质是什么?
类组件按组件实例的挂载、更新、卸载组织逻辑;Effect 则按“建立某个外部同步过程,以及何时清理/重建它”组织逻辑。Hooks 的重点是按关注点聚合 setup 和 cleanup,而不是逐个复刻类生命周期。